using System;
using System.Data;
using System.Text;
using System.Text.RegularExpressions;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using BlogEngine.Core.Web.Extensions;

/// <summary>
/// The <see cref="Snippets"/>
/// class finds preconfigured text values in posts, pages and comments and replaces them with html fragments.
/// </summary>
[Extension("Replaces text with html fragments", "2.0.0",
    "<a href='http://www.neovolve.com/' target='_blank'>Rory Primrose</a>")]
public class Snippets
{
    /// <summary>
    /// Defines the key used in the settings to identify html fragment values.
    /// </summary>
    private const String FragmentKey = "Fragment";

    /// <summary>
    /// Defines the key used in the settings to identify the requires brackets flag.
    /// </summary>
    private const String RequireBracketsKey = "RequireBrackets";

    /// <summary>
    /// Defines the key used in the settings to identify the text to find as content is served.
    /// </summary>
    private const String TextToFindKey = "TextToFind";

    /// <summary>
    /// Defines the expression used to identify anchor tags.
    /// </summary>
    private static readonly Regex FindAnchorExpression = new Regex(
        @"<a[^>]+>", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>
    /// Defines the expression used to identify tags and their subsequent contents.
    /// </summary>
    private static readonly Regex FindTagAndContentsExpression =
        new Regex(@"(?<Tag><(?<TagName>/?\w+)[^>]*>)(?<Contents>[^<>]*)", RegexOptions.Compiled | RegexOptions.Multiline);

    /// <summary>
    /// Stores the settings for the extension.
    /// </summary>
    private static ExtensionSettings _settings;

    /// <summary>
    /// Initializes a new instance of the <see cref="Snippets"/> class.
    /// </summary>
    public Snippets()
    {
        // Hook up the post, page and comment serving events
        Post.Serving += ProcessEntry;
        Page.Serving += ProcessEntry;
        Comment.Serving += ProcessEntry;

        // Load the settings for the extension
        LoadSettings();
    }

    /// <summary>
    /// Formats the contents.
    /// </summary>
    /// <param name="body">The body of the item.</param>
    /// <returns>A <see cref="String"/> value.</returns>
    public static String FormatContents(String body)
    {
        DataTable table = _settings.GetDataTable();

        // Loop through each setting record
        for (Int32 index = 0; index < table.Rows.Count; index++)
        {
            DataRow row = table.Rows[index];
            Boolean requireBrackets;
            String textToFind = row[TextToFindKey].ToString();
            String fragment = row[FragmentKey].ToString();

            // Use a try parse here just incase the settings get corrupted
            // This avoids exceptions being raised by making an assumption that brackets are not required
            Boolean.TryParse(row[RequireBracketsKey].ToString(), out requireBrackets);

            body = ProcessFindText(body, textToFind, fragment, requireBrackets);
        }

        return body;
    }

    /// <summary>
    /// Injects the fragment.
    /// </summary>
    /// <param name="body">The body of the item.</param>
    /// <param name="textToFind">The text to find.</param>
    /// <param name="fragment">The fragment to inject.</param>
    /// <param name="requireBrackets">if set to <c>true</c> [require brackets].</param>
    /// <returns>A <see cref="String"/> value.</returns>
    private static String InjectFragment(String body, String textToFind, String fragment, Boolean requireBrackets)
    {
        String safeFindExpression = Regex.Escape(textToFind);

        // Replace any values delimited with brackets
        String newBody = ReplaceBracketValues(safeFindExpression, body, fragment);

        // Check if brackets are required
        if (requireBrackets)
        {
            // We don't need to search for combinations without brackets
            return newBody;
        }

        // Replace any values that are not delimited with brackets
        newBody = ReplaceNonBracketValues(safeFindExpression, newBody, fragment);

        return newBody;
    }

    /// <summary>
    /// Processes the entry.
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.</param>
    private static void ProcessEntry(Object sender, ServingEventArgs e)
    {
        String body = e.Body;
        body = FormatContents(body);

        // Assign the body back to the event arguments
        e.Body = body;
    }

    /// <summary>
    /// Processes the find text.
    /// </summary>
    /// <param name="body">The body of the item.</param>
    /// <param name="textToFind">The text to find.</param>
    /// <param name="fragment">The fragment to inject.</param>
    /// <param name="requireBrackets">if set to <c>true</c> [require brackets].</param>
    /// <returns>A <see cref="String"/> value</returns>
    private static String ProcessFindText(String body, String textToFind, String fragment, Boolean requireBrackets)
    {
        String[] delimiters = new String[]
                                  {
                                      ";",
                                      ","
                                  };
        String[] textParts = textToFind.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);

        // Loop through each text part to find
        for (Int32 index = 0; index < textParts.Length; index++)
        {
            // Get the next part to process
            String part = textParts[index].Trim();

            // Check if there is a value
            if (String.IsNullOrEmpty(part))
            {
                continue;
            }

            // Replace the text with the fragment for this setting
            body = InjectFragment(body, part, fragment, requireBrackets);
        }

        return body;
    }

    /// <summary>
    /// Replaces the bracket values.
    /// </summary>
    /// <param name="findExpression">The expression to find.</param>
    /// <param name="body">The body of the item.</param>
    /// <param name="fragment">The fragment to inject.</param>
    /// <returns>A <see cref="String"/> value.</returns>
    private static String ReplaceBracketValues(String findExpression, String body, String fragment)
    {
        String bracketExpression = @"\[" + findExpression + @"\]";

        // We will always look for text surrounded in brackets and replace it

        // Replace all items that are surround by brackets
        body = Regex.Replace(body, bracketExpression, fragment, RegexOptions.Singleline | RegexOptions.IgnoreCase);

        return body;
    }

    /// <summary>
    /// Replaces the non bracket values.
    /// </summary>
    /// <param name="findExpression">The expression to find.</param>
    /// <param name="body">The body of the item.</param>
    /// <param name="fragment">The fragment to inject.</param>
    /// <returns>A <see cref="String"/> value.</returns>
    private static String ReplaceNonBracketValues(String findExpression, String body, String fragment)
    {
        // This replacement allows for non-bracket text to be replaced
        // Replace items found that are not contained in other words
        // Word and line boundary checks are made. To cover the case when a non-word character is in the find expression, 
        // lookarounds for non-word characters are also included in the expression
        String noBracketExpressionValue = @"(^|\b|(?<=\W))" + findExpression + @"(\b|(?=\W)|$)";
        Regex noBracketExpression = new Regex(noBracketExpressionValue, RegexOptions.Multiline | RegexOptions.IgnoreCase);

        // Find all the tag and content matches
        MatchCollection tagsAndContents = FindTagAndContentsExpression.Matches(body);

        if (tagsAndContents.Count == 0)
        {
            // There are no tags in the content, we can just do a replace of the matches
            return noBracketExpression.Replace(body, fragment);
        }

        Boolean protectAnchorContent = FindAnchorExpression.IsMatch(fragment);
        Boolean isSupportedTag = true;
        String skippedTagName = String.Empty;
        StringBuilder newBody = new StringBuilder();

        // Get the first tag
        Match firstTagAndContent = tagsAndContents[0];

        // Check if there is text before the tag
        if (firstTagAndContent.Index > 0)
        {
            newBody.Append(body.Substring(0, firstTagAndContent.Index));
        }

        // Loop through each tag
        for (Int32 index = 0; index < tagsAndContents.Count; index++)
        {
            Match tagAndContent = tagsAndContents[index];

            // Get the tag name
            String tagName = tagAndContent.Groups["TagName"].Value.ToUpperInvariant();

            // Check whether the next tag should be supported
            if (isSupportedTag)
            {
                // Check if anchor content should be protected from updates with fragments
                // This is required where the fragment to insert contains an anchor itself
                if (protectAnchorContent
                    && tagName == "A")
                {
                    skippedTagName = tagName;
                    isSupportedTag = false;
                }
                else if (tagName == "PRE")
                {
                    skippedTagName = tagName;
                    isSupportedTag = false;
                }
            }

            // Check whether we should stop skipping the tag
            if (isSupportedTag == false
                    && tagName == @"/" + skippedTagName)
            {
                // This is the end of the tag being skipped
                isSupportedTag = true;
                skippedTagName = String.Empty;
            }

            if (isSupportedTag == false)
            {
                // Add this segment of the body
                newBody.Append(tagAndContent.Value);

                // Skip to the next match
                continue;
            }

            // We are not in an anchor

            // Get the tag and contents
            String tag = tagAndContent.Groups["Tag"].Value;
            String existingContents = tagAndContent.Groups["Contents"].Value;

            // Add the tag
            newBody.Append(tag);

            String updatedContents = noBracketExpression.Replace(existingContents, fragment);

            newBody.Append(updatedContents);
        }

        return newBody.ToString();
    }

    /// <summary>
    /// Loads the settings.
    /// </summary>
    private void LoadSettings()
    {
        ExtensionSettings defaultSettings = new ExtensionSettings(this);

        // Define the structure for the settings
        defaultSettings.IsScalar = false;
        defaultSettings.AddParameter(TextToFindKey, "The text to find and replace", 50, true, true);
        defaultSettings.AddParameter(FragmentKey, "The fragment to insert", 250, true);
        defaultSettings.AddParameter(
            RequireBracketsKey, "Require brackets on text to find", 1, true, false, ParameterType.Boolean);

        defaultSettings.Help =
            @"<b>Text to find:</b> Will be the text that will be replaced by the fragment<br/>
                                 <b>Fragment:</b> The html fragment to insert<br/>
                                 <b>Require Brackets:</b> True if the text to find must be surrounded by [] otherwise False will match brackets and no brackets.<br/>
                                 Matching text without brackets will only match text not contained in another word.<br/>";

        // Add a default record
        defaultSettings.AddValue(TextToFindKey, "BE");
        defaultSettings.AddValue(
            FragmentKey, @"<a href='http://www.dotnetblogengine.net' target='_blank'>BlogEngine.Net</a>");
        defaultSettings.AddValue(RequireBracketsKey, true);

        // Initialize the settings
        _settings = ExtensionManager.InitSettings(GetType().Name, defaultSettings);
    }
}